/* Document.cpp
Copyright (c) 2008 Michael Zahniser
Please note the license terms (MIT license) at the end of this document.

Function definitions for the Hexie Document class.
*/

#include "Document.h"

#include <fstream>

using std::ifstream;
using std::ofstream;
using std::ios;
using std::streamsize;



// Default constructor:
// Creates an empty document.
Document::Document()
	: cursor(begin()), anchor(begin()), fileName("no file loaded")
{
}



// Load:
// Load the given file. If the file name is in the format "file.ext.hexie",
// the actual data is loaded from "file.ext" instead. If a "file.ext.hexie"
// exists, markup is read from it.
void Document::Load(const string &path)
{
	if(path.length() > 6 && path.substr(path.length() - 6, 6) == ".hexie")
		Load(path.substr(0, path.length() - 6));
	else
	{
		if(!ReadFile(path))
			return;

		// This is the path to the markup file, which may already exist.
		markupFilePath = path + ".hexie";
		// This is the file name.
		size_t pos = path.rfind('/');
		if(pos == string::npos)
			pos = 0;
		else
			++pos;
		fileName = path.substr(pos, string::npos);
		
		ReadMarkup();
	}
}



// Save:
// Save the markup for the current file as "current/file/path.ext.hexie".
void Document::Save() const
{
	ofstream out(markupFilePath.c_str(), ios::out | ios::binary);
	
	// First, save the cursor and anchor.
	WriteValue(cursor - begin(), out);
	WriteValue(anchor - begin(), out);
	
	// Next, save the global data type.
	// For the global type, don't bother to save its total bytes.
	out.put(defaultType.DataType());
	out.put('0' + defaultType.ElementSize());
	
	// Now, save all the markup and breaks.
	map<It, Type>::const_iterator markupIt = markup.begin();
	set<It>::const_iterator breaksIt = breaks.begin();
	It previous = begin();
	
	// Until one of the two is at the end, interleave them.
	while(markupIt != markup.end() && breaksIt != breaks.end())
	{
		bool markupIsNext = (markupIt->first < *breaksIt);
		It next = (markupIsNext ? markupIt->first : *breaksIt);
		WriteValue(next - previous, out);
		if(markupIsNext)
		{
			out.put(markupIt->second.DataType());
			out.put('0' + markupIt->second.ElementSize());
			WriteValue(markupIt->second.TotalBytes(), out);
			++markupIt;
		}
		else
		{
			out.put('\n');
			++breaksIt;
		}
		previous = next;
	}
	
	// If we enter this loop, only the markup remains to be written.
	while(markupIt != markup.end())
	{
		WriteValue(markupIt->first - previous, out);
		out.put(markupIt->second.DataType());
		out.put(markupIt->second.ElementSize());
		WriteValue(markupIt->second.TotalBytes(), out);
		previous = markupIt->first;
		++markupIt;
	}
	
	// If we enter this loop, only the breaks remain to be written.
	while(breaksIt != breaks.end())
	{
		WriteValue(*breaksIt - previous, out);
		out.put('\n');
		previous = *breaksIt;
		++breaksIt;
	}
}



// FileName:
// Get the name of the file.
const string &Document::FileName() const
{
	return fileName;
}



// Begin:
// Get an iterator to the start of the document.
Document::It Document::Begin() const
{
	return begin();
}



// End:
// Get an iterator to the end of the document.
Document::It Document::End() const
{
	return end();
}



// Size:
// Get the size of the document, in bytes.
size_t Document::Size() const
{
	return size();
}



// Cursor:
// Get the cursor position.
Document::It Document::Cursor() const
{
	return cursor;
}



// SelectionBegin:
// Get an iterator to the beginning of the selection. This may be either
// the anchor or the cursor, depending on which comes first.
Document::It Document::SelectionBegin() const
{
	return (anchor < cursor) ? anchor : cursor;
}



// SelectionEnd:
// Get the end of the selection, inclusive.
// If only the cursor is selected, selection begin = end = cursor.
Document::It Document::SelectionEnd() const
{
	return (anchor > cursor) ? anchor : cursor;
}



// SetCursor:
// Move the cursor. If keepAnchor, the selection expands to include the
// cursor position. Otherwise, the selection becomes the cursor position.
void Document::SetCursor(It position, bool keepAnchor)
{
	cursor = position;
	
	if(cursor < begin())
		cursor = begin();
	else if(cursor > end())
		cursor = end();
		
	if(!keepAnchor)
		anchor = cursor;
}



// NextCursor:
// Move the cursor to the end of the current markup, if any.
// As with SetCursor, this optionally preserves the anchor point.
void Document::NextCursor(bool keepAnchor)
{
	map<It, Type>::const_iterator it = MarkupOf(cursor);
	if(it == markup.end())
		SetCursor(cursor + 1, keepAnchor);
	else
		SetCursor(it->first + it->second.TotalBytes(), keepAnchor);
}



// Handle:
// Handle a key press by the user. In general, letters map to data types,
// numbers to element sizes, '\b' to delete, and '\n' to line break.
// Returns true if the key is recognized and handled.
// If isGlobal, this key affects the default style, not the currently
// selected data.
bool Document::Handle(char key, bool isGlobal)
{
	if(key >= 'a' && key <= 'z')
	{
		// Check that the given type is valid.
		if(!Type().SetDataType(key))
			return false;
		
		if(isGlobal)
			defaultType.SetDataType(key);
		else
		{
			It first = SelectionBegin();
			It last = SelectionEnd();
			
			// Don't create markup for the end of the file.
			if(first == end())
				return false;
			else if(last != end())
				++last;
			
			// Erase all the markup in the current selection, except for the current
			// byte's markup.
			Type thisType = markup[first];
			DeleteMarkup();
				
			// Set the markup for the current selection.
			thisType.SetDataType(key);
			thisType.SetTotalBytes(static_cast<int>(last - first));
			
			markup[first] = thisType;
		}
	}
	else if(key >= '0' && key <= '9')
	{
		if(isGlobal)
			defaultType.SetElementSize(key - '0');
		else
		{
			// Set the markup area containing the cursor, if any, to the given element
			// size. If the cursor is not in any area, do nothing.
			
			// TODO: Setting the element size larger can cause markup to suddenly overlap
			// the next markup. What to do in these situations?
			map<It, Type>::iterator it = MarkupOf(cursor);
			if(it == markup.end())
				return false;
			if(!it->second.SetElementSize(key - '0'))
				return false;
		}
	}
	else if(key == '\b')
	{
		DeleteMarkup();
		DeleteBreaks();
	}
	else if(key == '\n')
		breaks.insert(cursor);
	
	return true;
}



// DefaultType:
// Get the default type.
const Type &Document::DefaultType() const
{
	return defaultType;
}



// Markup:
// Get the map of markup.
const map<Document::It, Type> &Document::Markup() const
{
	return markup;
}



// Breaks:
// Get the set of line breaks.
const set<Document::It> &Document::Breaks() const
{
	return breaks;
}



// MarkupOf:
// Get the markup (if any) of the given position.
// If there is no user-defined markup, markup.end() is returned.
map<Document::It, Type>::const_iterator Document::MarkupOf(It position) const
{
	map<Document::It, Type>::const_iterator it = markup.upper_bound(position);
	// it is the first element greater than the given position.
	// Therefore, if any markup contains the given position, it will be the
	// element immediately before it.
	if(it != markup.begin())
	{
		--it;
		// If markup were empty, the above guard would have caught it. So it
		// points to a valid element of markup. Check if that element contains
		// the given position.
		// We already know it->first <= position.
		if(it->first + it->second.TotalBytes() > position)
			return it;
	}
		
	// This markup contains the given position.
	return markup.end();
}



// MarkupOf:
// Get a non-const iterator to the markup (if any) of the given position.
map<Document::It, Type>::iterator Document::MarkupOf(It position)
{
	// Note: this function is a non-const copy of the previous function.
	map<Document::It, Type>::iterator it = markup.upper_bound(position);
	// it is the first element greater than the given position.
	// Therefore, if any markup contains the given position, it will be the
	// element immediately before it.
	if(it != markup.begin())
	{
		--it;
		// If markup were empty, the above guard would have caught it. So it
		// points to a valid element of markup. Check if that element contains
		// the given position.
		// We already know it->first <= position.
		if(it->first + it->second.TotalBytes() > position)
			return it;
	}
		
	// This markup contains the given position.
	return markup.end();
}



// ReadFile:
// Read the data from the given path. Return false if unable to read it.
bool Document::ReadFile(const string &path)
{
	// Open the file.
	ifstream in(path.c_str(), ios::in | ios::binary);
	if(!in)
		return false;

	// Determine the size of the file.
	in.seekg(0, ios::end);
	streamsize size = in.tellg();
	in.seekg(0, ios::beg);

	// Read the file into a temporary buffer, then copy that buffer into the string.
	// This is faster than just about anything but reading the file directly into
	// the string, which can technically be done but is not standard-compliant.
	char *buffer = new char[size];
	in.read(buffer, size);
	assign(buffer, size);
	delete [] buffer;
	
	return true;
}



// ReadMarkup:
// Read the markup from markupPath. If the file does not exist, start
// out with no markup entered.
void Document::ReadMarkup()
{
	// Clear any existing markup.
	markup.clear();
	breaks.clear();
	// Set the cursor to the beginning of the file.
	// This is the default if no markup is read.
	cursor = begin();
	anchor = begin();

	// Open the markup file, if it exists.
	ifstream in(markupFilePath.c_str(), ios::in | ios::binary);
	if(!in)
		return;
	
	// Read the cursor and anchor offsets.
	cursor += ReadValue(in);
	anchor += ReadValue(in);
	
	// Read the global data type.
	defaultType.SetDataType(in.get());
	defaultType.SetElementSize(in.get() - '0');
	
	// Read all markup and breaks.
	It it = begin();
	while(in)
	{
		it += ReadValue(in);
		char next = in.get();
		if(next == '\n')
			breaks.insert(it);
		else
		{
			markup[it].SetDataType(next);
			markup[it].SetElementSize(in.get() - '0');
			markup[it].SetTotalBytes(static_cast<int>(ReadValue(in)));
		}
	}
}



// DeleteMarkup:
// Delete all markup contained in the current selection.
void Document::DeleteMarkup()
{
	map<It, Type>::iterator first = markup.upper_bound(SelectionBegin());
	if(first != markup.begin())
	{
		--first;
		if(first->first + first->second.TotalBytes() <= SelectionBegin())
			++first;
	}
	// first now points to the first markup containined in the range beginning
	// with SelectionBegin. It may possibly be beyond SelectionEnd(), however.
	map<It, Type>::iterator last = markup.upper_bound(SelectionEnd());
	
	// Now, [first, last) is all the iterators contained in the selection.
	markup.erase(first, last);
}



// DeleteBreaks:
// Delete all breaks in the current selection.
void Document::DeleteBreaks()
{
	// Find the first iterator "first" such that *first >= SelectionBegin().
	set<It>::iterator first = breaks.lower_bound(SelectionBegin());
	// Find the first iterator "last" such that *last > SelectionEnd().
	set<It>::iterator last = breaks.upper_bound(SelectionEnd());
	
	// Now, [first, last) is all the iterators contained in the selection.
	breaks.erase(first, last);
}



// WriteValue:
// Write a number to the given output stream, using multiple bytes if necessary.
void Document::WriteValue(size_t value, ostream &out)
{
	static char bytes[9];
	char *it = bytes + 8;
	
	*it = static_cast<char>(value & 0x7F);
	
	while(value >>= 7)
		*--it = static_cast<char>(0x80 | (value & 0x7F));
	
	out.write(it, static_cast<streamsize>((bytes + 9) - it));
}



// ReadValue:
// Read a number that was written using the Write() function above.
size_t Document::ReadValue(istream &in)
{
	size_t value = 0;
	size_t next = 0;
	while(in && (next = static_cast<unsigned char>(in.get())) > 0x7F)
	{
		value |= (next & 0x7F);
		value <<= 7;
	}
	value |= next;
	
	return value;
}



/* Copyright (c) 2008 Michael Zahniser

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE. */
